#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
strings.py - translate the csv language file to different platforms

script usage:

    python strings.py -h
    python strings.py -c strings.csv
    python strings.py -c strings.csv -p android,ios

support platforms: android, ios, mac, php

translation format: first row must start with 'KEY'

    # Start with '#' to comment.

    # The 'KEY' row are language names.
    KEY         en_us               zh_cn   ...

    # The following rows are each field translations.
    NAME        name                名称
    ...

string format: could use the "n$" positional specifiers

                standard     ios|mac      android      php
    string      %s, %1$s     %@, %1$@     %s, %1$s      -
                %@, %1$@     %@, %1$@     %s, %1$s      -
    int         %d, %1$d     %d, %1$d     %d, %1$d      -
    double      %.2f, %1.2f  %.2f, %1.2f  %.2f, %1.2f   -

    # https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html
    # https://developer.android.com/reference/java/util/Formatter.html

special format:

            ios|mac     android     php
    &       &           &amp;       &amp;
    '       \'          \'          \'
    "       \"          \"          \"

    # https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling

build-in fields: plat, lang, empty

                symbol      description
    plat        #plat#      current platform name
    lang        #lang#      current language name
    empty       #empty#     keep empty fileds from being ignored

    # '#plat# and #lang#' will be translated to 'ios and en_us' etc.
    TEST        #plat# and #lang#   #plat#与#lang#
    ...

empty fileds: use '-k', '-i' options to keep or ignore them

    # Set '#empty#' to keep specified empty fileds from being ignored

    # Use '-d' to display details about ignored keys

if statement:

    # Use option '-v wind,rain' to add the extra condition values.
    # By default, only current platform name is included.

    # Start with '#[if,elif,else,endif]' to control the output flow.
    #if android
    ...(android)
    #else
    ...(except android)
    #endif
    ...

    # Even use it nested.
    #if android
    ...(android)
    #if wind
    ...(android,wind)
    #elif rain
    ...(android,rain)
    #endif
    ...(android)
    #endif

more features: see -h.
"""
import csv
import optparse
import os
import re
import string
import sys

if sys.version_info.major <= 2:
    import ConfigParser as configparser
else:
    import configparser


WHITESPACE = string.whitespace

PLATFORMS = ('android', 'ios', 'mac', 'php')


class PlatMeta(type):

    def __new__(cls, name, bases, attrs):
        if 'SPECIFIERS' in attrs:
            attrs['SPECIFIERS_PATTERN'] = \
                r'(?P<prefix>[^%]?%)(?P<position>\d\$)?(?P<token>[{tokens}])' \
                .format(tokens=''.join(attrs['SPECIFIERS'].keys()))
        return super(PlatMeta, cls).__new__(cls, name, bases, attrs)


# Writing Python 2-3 compatible code:
#   http://python-future.org/compatible_idioms.html
#
# from six import with_metaclass
# from future.utils import with_metaclass
# class Plat(with_metaclass(PlatMeta, object)):
#     pass


class Plat(object):
    """Platform."""

    __metaclass__ = PlatMeta

    SUFFIX = '.strings'
    # format specifiers
    SPECIFIERS = {
        's': '@',
    }
    # replacements: [('pattern', 'repl'), ...]
    REPLACEMENTS = [
        (r'(?<!\\)\'', '\\\''),
        (r'(?<!\\)"', '\\"'),
    ]

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def head(self):
        return None

    def tail(self):
        return None

    def key_value(self, key, value):
        return '%s = "%s";' % (key, self._format_value(value))

    def key_value_ignore(self, key, value):
        return '//%s = "%s";' % (key, self._format_value(value))

    def comment(self, comment):
        return '\n// %s\n' % comment

    def specialize(self, outdir, outfile, lang_name, lang_trans):
        lang_new = lang_trans[lang_name]
        if not lang_new: return False, None
        return os.path.join(outdir, lang_new + '.lproj'), 'Localizable' + self.SUFFIX

    @classmethod
    def _format_value(cls, value):
        value = cls._replace(value)
        if not cls.SPECIFIERS:
            return value
        return re.sub(cls.SPECIFIERS_PATTERN, cls._repl_specifiers, value)

    @classmethod
    def _repl_specifiers(cls, matchobj):
        prefix = matchobj.group('prefix')
        position = matchobj.group('position')
        token = matchobj.group('token')
        token = cls.SPECIFIERS.get(token, token)
        if position:
            return prefix + position + token
        else:
            return prefix + token

    @classmethod
    def _replace(cls, value):
        if cls.REPLACEMENTS:
            for k, v in cls.REPLACEMENTS:
                value = re.sub(k, v, value)
        return value


class PlatIOS(Plat):
    pass


class PlatMac(Plat):
    pass


class PlatAndroid(Plat):

    SUFFIX = '.xml'
    SPECIFIERS = {
        '@': 's',
    }
    REPLACEMENTS = Plat.REPLACEMENTS + [
        (r'&(?!amp;)', '&amp;'),
    ]

    def __init__(self, name):
        super(PlatAndroid, self).__init__(name)

    def head(self):
        return '<?xml version="1.0" encoding="utf-8"?>\n<resources>'

    def tail(self):
        return '</resources>'

    def key_value(self, key, value):
        return '    <string name="%s">%s</string>' % (key, self._format_value(value))

    def key_value_ignore(self, key, value):
        return '    <!--<string name="%s">%s</string>-->' % (key, self._format_value(value))

    def comment(self, comment):
        return '\n    <!--%s-->\n' % comment

    def specialize(self, outdir, outfile, lang_name, lang_trans):
        lang_new = lang_trans[lang_name]
        if not lang_new: lang_new = lang_name
        # move default lang to 'values/strings.xml'
        lang_def = lang_trans['default'] if lang_trans.has_key('default') else 'en'
        lang_new = '' if lang_new == lang_def else '-' + lang_new
        return os.path.join(outdir, 'values' + lang_new), 'strings' + self.SUFFIX


class PlatPHP(Plat):

    SUFFIX = '.php'
    SPECIFIERS = {}
    REPLACEMENTS = Plat.REPLACEMENTS + [
        (r'&(?!amp;)', '&amp;'),
    ]

    def __init__(self, name):
        super(PlatPHP, self).__init__(name)

    def head(self):
        return '<?php'

    def key_value(self, key, value):
        return '$lang[\'%s\'] = \'%s\';' % (key, self._format_value(value))

    def key_value_ignore(self, key, value):
        return '// $lang[\'%s\'] = \'%s\';' % (key, self._format_value(value))

    def specialize(self, outdir, outfile, lang_name, lang_trans):
        lang_new = lang_trans[lang_name]
        if not lang_new: lang_new = lang_name
        return os.path.join(outdir, lang_new), 'web_lang' + self.SUFFIX


class Lang(object):
    """Language."""

    def __init__(self, index, name):
        self.index = index
        self.name = name

    def __str__(self):
        return self.name


def do_cond(func):
    def _do_cond(cond, *args):
        func(cond, *args)
        print('%10s: %20s, ifs: %20s, accept: %s' %
            (func.__name__, args, str(cond._ifs), cond._accept))
    return _do_cond


class If(object):
    """If statement."""

    def __init__(self):
        self._accepts = []
        self._done = False

    def append(self, accept):
        """Append a branch condition result."""
        self._accepts.append(accept)
        if not self._done:
            self._done = accept

    def accepted(self):
        """Is current branch accepted."""
        return self._accepts[-1]

    def done(self):
        """Have already accepted a branch."""
        return self._done

    def __repr__(self):
        return str(self)

    def __str__(self):
        symbols = [('T' if a else 'F') for a in self._accepts]
        return '(%s,%s)' % (''.join(symbols), ('Y' if self._done else 'N'))


class Condition(object):
    """Condition processor."""

    def __init__(self, values):
        self.values = values
        self._ifs = []
        self._accept = True

    def process(self, content, line_num):
        do_func = None
        do_args = ()
        if content.startswith('if '):
            do_func = self._do_if
            do_args = self._get_fields('if ', content, line_num)
        elif content.startswith('elif '):
            do_func = self._do_elif
            do_args = self._get_fields('elif ', content, line_num)
        elif content == 'else':
            do_func = self._do_else
        elif content == 'endif':
            do_func = self._do_endif
        else:
            return False
        do_func(*do_args)
        self._update()
        self.prev_func = do_func
        return True

    def accept(self):
        return self._accept

    def _accept_fields(self, fields):
        for f in fields:
            if f in self.values:
                return True
        return False

    def _get_fields(self, prefix, content, line_num):
        expression = content[len(prefix):].strip(WHITESPACE)
        if expression:
            return expression.split(',')
        else:  # empty
            raise RuntimeError('Please specify the condition values, line %d' % line_num)

    def _update(self):
        if self._ifs:
            for _if in self._ifs:
                if not _if.accepted():
                    self._accept = False
                    return
        self._accept = True

    # @do_cond
    def _do_if(self, *fields):
        _if = If()
        _if.append(self._accept_fields(fields))
        self._ifs.append(_if)

    # @do_cond
    def _do_elif(self, *fields):
        _if = self._ifs[-1]
        if _if.done():
            _if.append(False)
        else:
            _if.append(self._accept_fields(fields))

    # @do_cond
    def _do_else(self):
        _if = self._ifs[-1]
        _if.append(not _if.done())

    # @do_cond
    def _do_endif(self):
        self._ifs.pop()


class Channel(object):
    """Channel for outputing to a file."""

    _BUILD_IN = r'(?<!\\)#(?P<key>[^#\s]+?)(?<!\\)#'

    def __init__(self, plat, lang, settings):
        # platform
        self.plat = plat
        # language
        self.lang = lang
        # settings
        self.settings = settings
        # build-in fileds
        self.fields = {
            'plat': plat.name,
            'lang': lang.name,
            'empty': '',
        }
        # condition
        values = [plat.name]
        values.extend(settings.values)
        self.cond = Condition(values)
        # file
        self.file = None
        self.post_action = None
        # empty cell count
        self.empty = 0
        self.empty_details = []

    def open(self, filename):
        path = os.path
        head, tail = path.split(filename)
        name, ext = path.splitext(tail)
        # output to directory of platform name
        outdir = path.join(head, self.plat.name)
        outfile = name + '_' + self.lang.name + self.plat.SUFFIX
        outdir, outfile = self._specialize(outdir, outfile)
        outpath = path.join(outdir, outfile)
        if not path.exists(outdir):
            os.makedirs(outdir)
        # print('>> %s' % outpath)
        self.file = open(outpath, 'w')
        self.head()

    def close(self):
        if self.file:
            self.tail()
            self.file.close()
            print('>> %s' % self.file.name)
            self._output_details()
            if self.post_action:
                self.post_action()
                self.post_action = None
            self.file = None

    def head(self):
        head = self.plat.head()
        if head:
            self._write(head)

    def tail(self):
        tail = self.plat.tail()
        if tail:
            self._write(tail)

    def comment(self, comment, line_num):
        if self.cond.process(comment, line_num):
            # process condition
            return
        # process comment if not condition
        if self.cond.accept():
            self._write(self.plat.comment(comment))

    def key_value(self, key, value, line_num):
        if not self.cond.accept():
            return
        if value or self.settings.keepempty:
            key_value = self.plat.key_value
        else:
            key_value = self.plat.key_value_ignore
            self.empty = self.empty + 1
            if self.settings.detail:
                self.empty_details.append((line_num, key))
        # replace build-in fileds
        value = self._repl_fields(value)
        # organize a key-value row
        value = key_value(key, value)
        # write to the output file
        self._write(value)

    def _write(self, str):
        # assert self.file, 'file not opened'
        if not self.file: return
        self.file.write(str)
        self.file.write('\n')

    def _repl_fields(self, value):
        if not value: return value
        return re.sub(self._BUILD_IN, self._repl_match, value)

    def _repl_match(self, matchobj):
        key = matchobj.group('key')
        value = self.fields.get(key, None)
        if value is None:
            print('warn: unspecfied build-in key \'%s\'' % key)
            value = '#%s#' % key
        return value

    def _specialize(self, outdir, outfile):
        if self.settings.special and \
                self.settings.special.has_key(self.plat.name):
            lang_trans = self.settings.special[self.plat.name]
            if not lang_trans: return outdir, outfile

            outdir, outfile = self.plat.specialize(outdir, outfile,
                self.lang.name.lower(), lang_trans)
            if not outdir: return False, None

            # move copy lang to 'Base.lproj/Localizable.strings' for ios, mac
            if self.plat.name in ('ios', 'mac'):
                lang_def = lang_trans['default'] if lang_trans.has_key('default') else 'en'
                if self.lang.name == lang_def:
                    def copy_action():
                        from os import path
                        import shutil
                        d = path.join(path.dirname(outdir), 'Base.lproj')
                        if not path.exists(d): os.makedirs(d)
                        shutil.copy(path.join(outdir, outfile), d)
                    self.post_action = copy_action
        return outdir, outfile

    def _output_details(self):
        if not self.empty_details:
            if self.empty:
                print('   ignore %d empty fileds' % self.empty)
            return
        root, ext = os.path.splitext(self.file.name)
        outfile = root + '.details'
        print('   ignore %d empty fileds, log to %s' % (self.empty, outfile))
        with open(outfile, 'w') as f:
            f.write('>> %s\n>> ignore %d empty fileds\n' %
                (self.file.name, self.empty))
            f.writelines('%5d: %s\n' % d for d in self.empty_details)


class Settings(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


class Strings(object):
    """Strings for translating the csv file."""

    KEY         = 'KEY'
    COMMENT     = '#'
    STRIP_CHARS_COMMENT = COMMENT + WHITESPACE

    def __init__(self, csvfile, platforms, settings):
        self.csvfile = csvfile
        self.platforms = platforms
        self.settings = settings

    def translate(self):
        with open(self.csvfile, 'r') as f:
            reader = csv.reader(f)

            row = self._safe_next_row(reader)
            if not row:
                sys.exit('first row not found or incorrect format')
                return

            key, langs = self._parse_langs(row)
            if not key:
                sys.exit('\'%s\' not found, incorrect format?' % self.KEY)
                return
            if not langs:
                sys.exit('language name not found, incorrect format?')
                return

            idx_key = key[0]
            channels = self._build_channels(self.platforms, langs)

            try:
                while True:
                    row = self._safe_next(reader)
                    if not row:
                        break
                    key = row[idx_key]
                    if not key:
                        continue
                    line_num = reader.line_num
                    if key.startswith(self.COMMENT):
                        self._comment(channels, key, line_num)
                    else:
                        self._key_value(channels, key, row, line_num)
            except csv.Error as e:
                sys.exit('file %s, line %d: %s' % (self.csvfile, reader.line_num, e))
            finally:
                # ensure close all
                platname = None
                for c in channels:
                    if not platname:
                        platname = c.plat.name
                    elif platname != c.plat.name:
                        platname = c.plat.name
                        print
                    c.close()

            print('\nCOMPLETE!!!')

    def _safe_next(self, reader):
        try:
            return next(reader)
        except StopIteration:
            return None

    def _safe_next_row(self, reader):
        try:
            return next(reader)
        except (StopIteration, csv.Error) as e:
            sys.exit("error: %s" % e)
            return None

    def _parse_langs(self, lang_row):
        key = None
        langs = []
        for i, v in enumerate(lang_row):
            if v:
                if v == self.KEY:
                    key = (i, v)
                else:
                    langs.append((i, v))
        return key, langs

    def _build_channels(self, platforms, langs):
        channels = []
        for plat in platforms:
            for lang in langs:
                c = Channel(plat, Lang(*lang), self.settings)
                c.open(self.csvfile)
                channels.append(c)
                # break
        return channels

    def _comment(self, channels, comment, line_num):
        for c in channels:
            c.comment(comment.strip(self.STRIP_CHARS_COMMENT), line_num)

    def _key_value(self, channels, key, row, line_num):
        for c in channels:
            c.key_value(key, row[c.lang.index], line_num)


def _read_config(cfgfile):
    cfg = configparser.ConfigParser()
    cfg.read(cfgfile)
    ret = {}
    for sec in cfg.sections():
        ret[sec] = {opt: cfg.get(sec, opt) for opt in cfg.options(sec)}
    return ret


def _parse_args():
    parser = optparse.OptionParser()
    parser.add_option('-c', '--csv', dest='csvfile', metavar='file',
        help='the input csv|excel file path with translations')
    platforms = ','.join(PLATFORMS)
    parser.add_option('-p', '--platform', dest='platforms',
        metavar=platforms, default=platforms,
        help='the specified platforms which separated by comma [default: %default]')
    parser.add_option('-v', '--value', dest='values',
        metavar='wind,rain', default=[],
        help='the extra condition values which separated by comma'
             '\n(by default, only current platform name is included)')
    parser.add_option('-k', '--keep', dest='keepempty', default=False,
        action='store_true', help='keep empty fields [default: ignore]')
    parser.add_option('-i', '--ignore', dest='keepempty',
        action='store_false', default=False,
        help='ignore empty fields using comment [default: ignore]')
    parser.add_option('-d', '--detail', dest='detail',
        action='store_false', default=False,
        help='display details about ignored keys')
    parser.add_option('-s', '--special', dest='special', metavar='file',
        help='the input specialization file path')
    options, args = parser.parse_args()
    # positional arguments are ignored
    return options

# python
#   python --version
# get pip install script
#   curl -O https://bootstrap.pypa.io/get-pip.py
#   wget https://bootstrap.pypa.io/get-pip.py -q --show-progress
# install pip
#   sudo python get-pip.py
# install xlrd
#   sudo pip install xlrd

# http://stackoverflow.com/questions/26029095/python-convert-excel-to-csv
def _excel2csv(excelfile, csvfile, sheetname=None):
     import xlrd
     workbook = xlrd.open_workbook(excelfile)
     if not sheetname:
        sheetname = workbook.sheet_names()[0]
     worksheet = workbook.sheet_by_name(sheetname)

     csvfile = open(csvfile, 'wb')
     wr = csv.writer(csvfile, quoting=csv.QUOTE_ALL)

     for rownum in xrange(worksheet.nrows):
         wr.writerow(
             list(x.encode('utf-8') if type(x) == type(u'') else x
                  for x in worksheet.row_values(rownum)))

     csvfile.close()


def _detect_csvfile(path):
    root, ext = os.path.splitext(path)
    if ext == '.csv':
        return path
    elif ext in ('.xls', '.xlsx'):
        csvfile = root + '.csv'
        print('>> export %s to %s\n' % (path, csvfile))
        _excel2csv(path, csvfile)
        return csvfile
    else:
        sys.exit('not support this file format: %s' % ext)


def main():
    options = _parse_args()

    csvfile = options.csvfile
    if not csvfile or not os.path.exists(csvfile):
        sys.exit('not found the csv file: %s' % csvfile)

    csvfile = _detect_csvfile(csvfile)

    if not options.platforms:
        sys.exit('not specify the output platforms')
    platforms = [plat for plat in options.platforms.split(',') if plat in PLATFORMS]
    if not platforms:
        sys.exit('platforms must be specified from: ' + ','.join(PLATFORMS))

    plats = {
        PLATFORMS[0]: PlatAndroid,
        PLATFORMS[1]: PlatIOS,
        PLATFORMS[2]: PlatMac,
        PLATFORMS[3]: PlatPHP,
    }
    platforms = [(plats[p])(p) for p in platforms]

    settings = Settings(**{
        'values': options.values.split(',') if options.values else options.values,
        'keepempty': options.keepempty,
        'detail': options.detail,
        'special': _read_config(options.special) if options.special else None,
    })

    s = Strings(csvfile, platforms, settings)
    s.translate()

if __name__ == '__main__':
    main()
